/*
 * Copyright 2002-2021 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.hasor.cobble.reflect.resolvable;
import net.hasor.cobble.CollectionUtils;
import net.hasor.cobble.ExceptionUtils;
import net.hasor.cobble.ObjectUtils;
import net.hasor.cobble.ref.ConcurrentReferenceHashMap;
import net.hasor.cobble.reflect.MethodUtils;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.lang.reflect.*;
import java.util.Objects;

/**
 * Internal utility class that can be used to obtain wrapped {@link Serializable}
 * variants of {@link java.lang.reflect.Type java.lang.reflect.Types}.
 * <p>{@link #forField(Field) Fields} can be used as the root source for a serializable type.
 * Alternatively, a regular {@link Class} can also be used as source.
 * <p>The returned type will either be a {@link Class} or a serializable proxy of
 * {@link GenericArrayType}, {@link ParameterizedType}, {@link TypeVariable} or
 * {@link WildcardType}. With the exception of {@link Class} (which is final) calls
 * to methods that return further {@link Type Types} (for example
 * {@link GenericArrayType#getGenericComponentType()}) will be automatically wrapped.
 * 2021-11-01 reduction based on SpringFramework, org.springframework.core.SerializableTypeWrapper
 * @author 赵永春 (zyc@hasor.net)
 * @author Phillip Webb
 * @author Juergen Hoeller
 * @author Sam Brannen
 * @since 4.0
 */
class SerializableTypeWrapper {
    // See https://github.com/oracle/graal/blob/master/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/ImageInfo.java
    private static final boolean imageCode = (System.getProperty("org.graalvm.nativeimage.imagecode") != null);

    /** Returns {@code true} if invoked in the context of image building or during image runtime, else {@code false}. */
    public static boolean inNativeImage() {
        return imageCode;
    }

    private static final Class<?>[]                             SUPPORTED_SERIALIZABLE_TYPES = { GenericArrayType.class, ParameterizedType.class, TypeVariable.class, WildcardType.class };
    static final         ConcurrentReferenceHashMap<Type, Type> cache                        = new ConcurrentReferenceHashMap<>(256);

    private SerializableTypeWrapper() {
    }

    /** Return a {@link Serializable} variant of {@link Field#getGenericType()}. */
    public static Type forField(Field field) {
        return forTypeProvider(new FieldTypeProvider(field));
    }

    /**
     * Unwrap the given type, effectively returning the original non-serializable type.
     * @param type the type to unwrap
     * @return the original non-serializable type
     */
    public static <T extends Type> T unwrap(T type) {
        Type unwrapped = null;
        if (type instanceof SerializableTypeProxy) {
            unwrapped = ((SerializableTypeProxy) type).getTypeProvider().getType();
        }
        return (unwrapped != null ? (T) unwrapped : type);
    }

    /**
     * Return a {@link Serializable} {@link Type} backed by a {@link TypeProvider} .
     * <p>If type artifacts are generally not serializable in the current runtime
     * environment, this delegate will simply return the original {@code Type} as-is.
     */
    static Type forTypeProvider(TypeProvider provider) {
        Type providedType = provider.getType();
        if (providedType == null || providedType instanceof Serializable) {
            // No serializable type wrapping necessary (e.g. for java.lang.Class)
            return providedType;
        }
        if (inNativeImage()) {
            // Let's skip any wrapping attempts if types are generally not serializable in
            // the current runtime environment (even java.lang.Class itself, e.g. on GraalVM native images)
            return providedType;
        } else {
            Serializable.class.isAssignableFrom(Class.class);
        }

        // Obtain a serializable type proxy for the given provider...
        Type cached = cache.get(providedType);
        if (cached != null) {
            return cached;
        }
        for (Class<?> type : SUPPORTED_SERIALIZABLE_TYPES) {
            if (type.isInstance(providedType)) {
                ClassLoader classLoader = provider.getClass().getClassLoader();
                Class<?>[] interfaces = new Class<?>[] { type, SerializableTypeProxy.class, Serializable.class };
                InvocationHandler handler = new TypeProxyInvocationHandler(provider);
                cached = (Type) Proxy.newProxyInstance(classLoader, interfaces, handler);
                cache.put(providedType, cached);
                return cached;
            }
        }
        throw new IllegalArgumentException("Unsupported Type class: " + providedType.getClass().getName());
    }

    /**
     * Additional interface implemented by the type proxy.
     */
    interface SerializableTypeProxy {
        /**
         * Return the underlying type provider.
         */
        TypeProvider getTypeProvider();
    }

    /** A {@link Serializable} interface providing access to a {@link Type}. */
    interface TypeProvider extends Serializable {
        /**
         * Return the (possibly non {@link Serializable}) {@link Type}.
         */
        Type getType();

        /**
         * Return the source of the type, or {@code null} if not known.
         * <p>The default implementations returns {@code null}.
         */
        default Object getSource() {
            return null;
        }
    }

    /**
     * {@link Serializable} {@link InvocationHandler} used by the proxied {@link Type}.
     * Provides serialization support and enhances any methods that return {@code Type}
     * or {@code Type[]}.
     */
    private static class TypeProxyInvocationHandler implements InvocationHandler, Serializable {
        private final TypeProvider provider;

        public TypeProxyInvocationHandler(TypeProvider provider) {
            this.provider = provider;
        }

        @Override

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            switch (method.getName()) {
                case "equals":
                    Object other = args[0];
                    // Unwrap proxies for speed
                    if (other instanceof Type) {
                        other = unwrap((Type) other);
                    }
                    return Objects.deepEquals(this.provider.getType(), other);
                case "hashCode":
                    return ObjectUtils.nullSafeHashCode(this.provider.getType());
                case "getTypeProvider":
                    return this.provider;
            }

            if (Type.class == method.getReturnType() && CollectionUtils.isEmpty(args)) {
                return forTypeProvider(new MethodInvokeTypeProvider(this.provider, method, -1));
            } else if (Type[].class == method.getReturnType() && CollectionUtils.isEmpty(args)) {
                Type[] result = new Type[((Type[]) method.invoke(this.provider.getType())).length];
                for (int i = 0; i < result.length; i++) {
                    result[i] = forTypeProvider(new MethodInvokeTypeProvider(this.provider, method, i));
                }
                return result;
            }

            try {
                return method.invoke(this.provider.getType(), args);
            } catch (InvocationTargetException ex) {
                throw ex.getTargetException();
            }
        }
    }

    /** {@link TypeProvider} for {@link Type Types} obtained from a {@link Field}. */
    static class FieldTypeProvider implements TypeProvider {
        private final     String   fieldName;
        private final     Class<?> declaringClass;
        private transient Field    field;

        public FieldTypeProvider(Field field) {
            this.fieldName = field.getName();
            this.declaringClass = field.getDeclaringClass();
            this.field = field;
        }

        @Override
        public Type getType() {
            return this.field.getGenericType();
        }

        @Override
        public Object getSource() {
            return this.field;
        }

        private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
            inputStream.defaultReadObject();
            try {
                this.field = this.declaringClass.getDeclaredField(this.fieldName);
            } catch (Throwable ex) {
                throw new IllegalStateException("Could not find original class structure", ex);
            }
        }
    }

    /** {@link TypeProvider} for {@link Type Types} obtained from a {@link MethodParameter}. */
    static class MethodParameterTypeProvider implements TypeProvider {
        private final     String          methodName;
        private final     Class<?>[]      parameterTypes;
        private final     Class<?>        declaringClass;
        private final     int             parameterIndex;
        private transient MethodParameter methodParameter;

        public MethodParameterTypeProvider(MethodParameter methodParameter) {
            this.methodName = (methodParameter.getMethod() != null ? methodParameter.getMethod().getName() : null);
            this.parameterTypes = methodParameter.getExecutable().getParameterTypes();
            this.declaringClass = methodParameter.getDeclaringClass();
            this.parameterIndex = methodParameter.getParameterIndex();
            this.methodParameter = methodParameter;
        }

        @Override
        public Type getType() {
            return this.methodParameter.getGenericParameterType();
        }

        @Override
        public Object getSource() {
            return this.methodParameter;
        }

        private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
            inputStream.defaultReadObject();
            try {
                if (this.methodName != null) {
                    this.methodParameter = new MethodParameter(this.declaringClass.getDeclaredMethod(this.methodName, this.parameterTypes), this.parameterIndex);
                } else {
                    this.methodParameter = new MethodParameter(this.declaringClass.getDeclaredConstructor(this.parameterTypes), this.parameterIndex);
                }
            } catch (Throwable ex) {
                throw new IllegalStateException("Could not find original class structure", ex);
            }
        }
    }

    /** {@link TypeProvider} for {@link Type Types} obtained by invoking a no-arg method. */
    static class MethodInvokeTypeProvider implements TypeProvider {
        private final              TypeProvider provider;
        private final              String       methodName;
        private final              Class<?>     declaringClass;
        private final              int          index;
        private transient          Method       method;
        private transient volatile Object       result;

        public MethodInvokeTypeProvider(TypeProvider provider, Method method, int index) {
            this.provider = provider;
            this.methodName = method.getName();
            this.declaringClass = method.getDeclaringClass();
            this.index = index;
            this.method = method;
        }

        @Override
        public Type getType() {
            Object result = this.result;
            if (result == null) {
                // Lazy invocation of the target method on the provided type
                try {
                    result = this.method.invoke(this.provider.getType());
                } catch (IllegalAccessException | InvocationTargetException e) {
                    throw ExceptionUtils.toRuntime(e);
                }
                // Cache the result for further calls to getType()
                this.result = result;
            }
            return (result instanceof Type[] ? ((Type[]) result)[this.index] : (Type) result);
        }

        @Override
        public Object getSource() {
            return null;
        }

        private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
            inputStream.defaultReadObject();
            Method method = MethodUtils.getAccessibleMethod(this.declaringClass, this.methodName, new Class[0]);
            if (method == null) {
                throw new IllegalStateException("Cannot find method on deserialization: " + this.methodName);
            }
            if (method.getReturnType() != Type.class && method.getReturnType() != Type[].class) {
                throw new IllegalStateException("Invalid return type on deserialized method - needs to be Type or Type[]: " + method);
            }
            this.method = method;
        }
    }
}
